Skip to content

fix(session-log): walk ancestor PIDs to resolve correct session log#598

Merged
backnotprop merged 3 commits intobacknotprop:mainfrom
elithompson:eli/fix-458-session-log-ancestor-pid
Apr 21, 2026
Merged

fix(session-log): walk ancestor PIDs to resolve correct session log#598
backnotprop merged 3 commits intobacknotprop:mainfrom
elithompson:eli/fix-458-session-log-ancestor-pid

Conversation

@elithompson
Copy link
Copy Markdown
Contributor

Fixes #458

Problem

When plannotator is invoked from a slash command's ! bang, the direct parent process (process.ppid) is an intermediate bash shell spawned by the Bash tool — not Claude Code itself. The old resolveSessionLogByPpid() only checked process.ppid, so it always returned null for the session metadata lookup. The fallback findSessionLogsForCwd() then picks the most recently modified .jsonl for the project, which is wrong when multiple sessions exist for the same repo.

Empirically verified (as described in #458):

plannotator ppid: 44666  → sessions/44666.json: ✗  (bash subshell)
ancestor pid:    40595   → sessions/40595.json: ✓  (Claude Code — correct!)

Fix

Four-tier resolution ladder, from most to least reliable:

  1. Ancestor-PID walk (resolveSessionLogByAncestorPids): calls ps -o ppid= repeatedly from process.ppid, walking up to 8 hops and checking ~/.claude/sessions/<pid>.json at each one. Deterministic — matches the exact session without any guessing.
  2. Cwd-scan (resolveSessionLogByCwdScan): reads every ~/.claude/sessions/*.json, filters by cwd, picks the entry with the most recent startedAt. Handles environments where ps is unavailable and degrades better than mtime when multiple sessions exist.
  3. CWD slug mtime (legacy): existing behavior — picks the most recently modified .jsonl for the project slug. Fragile with multiple sessions, but kept as a fallback.
  4. Ancestor directory walk: existing behavior — handles the case where the user cd'd into a subdirectory after session start.

New exports (for test isolation)

  • getAncestorPids(startPid, maxHops, getParent) — injectable getParent for testing without real PIDs
  • resolveSessionLogByAncestorPids(opts) — injectable sessionsDir, projectsDir, getParentPid, startPid
  • resolveSessionLogByCwdScan(opts) — injectable sessionsDir, projectsDir, cwd
  • SessionMetadata exported (was private)
  • findSessionLogsForCwd(cwd, projectsDirOverride?) — optional override for test isolation

Tests

17 new tests in session-log.test.ts:

  • getAncestorPids: invalid startPid, no parent, walks chain, respects maxHops, breaks on cycles, breaks on self-loops
  • resolveSessionLogByAncestorPids: finds the correct session among multiple candidates, skips PIDs with no metadata, skips PIDs whose session log is missing, returns null when nothing matches
  • resolveSessionLogByCwdScan: picks the session with the newest startedAt when multiple exist, ignores sessions with mismatched cwd, returns null when sessions dir is missing

All tests use isolated tmpdirs and injected getParentPid — no real process tree or filesystem paths.

…data

When plannotator is invoked from a slash command's `!` bang, the direct
parent process is an intermediate bash shell spawned by the Bash tool —
not Claude Code itself. The old resolveSessionLogByPpid() only checked
process.ppid, so it always missed the session metadata file and fell back
to mtime-based selection, which picks the wrong log when multiple sessions
exist for the same project.

New resolution ladder (four tiers):
1. Ancestor-PID walk: call `ps -o ppid=` repeatedly from process.ppid up
   to 8 hops, checking ~/.claude/sessions/<pid>.json at each hop.
   Deterministic — no guessing, matches exact session every time.
2. Cwd-scan: read every ~/.claude/sessions/*.json, filter by cwd, pick
   most recent startedAt. Handles cases where ps is unavailable.
3. CWD slug mtime (legacy): existing behavior, fragile with multiple
   sessions.
4. Ancestor directory walk: handles cd-deeper-into-subdirectory cases.

Adds getAncestorPids (injectable getParent for testing), resolveSessionLogByAncestorPids,
and resolveSessionLogByCwdScan. Exports SessionMetadata and accepts
projectsDirOverride on findSessionLogsForCwd for test isolation.

17 new tests cover: edge cases for getAncestorPids (cycles, maxHops,
self-loops), resolveSessionLogByAncestorPids (finds correct session among
multiple, skips missing logs, falls back when no metadata matches), and
resolveSessionLogByCwdScan (picks newest startedAt, ignores mismatched
cwd, handles missing sessions dir).

Closes backnotprop#458
@elithompson elithompson force-pushed the eli/fix-458-session-log-ancestor-pid branch from 393e4be to e3e8f28 Compare April 21, 2026 17:36
@backnotprop
Copy link
Copy Markdown
Owner

Hey thanks! testing

Tier 1 used `ps`, which doesn't exist on Windows. Multiple Claude Code
sessions in the same repo remained broken on Windows because tier 2
(cwd scan) can't disambiguate when both sessions share the same cwd.

- Replace per-hop `ps` calls with a single process-table snapshot (`ps`
  on Unix, PowerShell Get-CimInstance on Windows), cached across the
  walk. One spawn instead of up to eight; faster on both platforms.
- Export pure-function parsers (parseProcessTablePs,
  parseProcessTableCsv) so the Windows path is unit-testable without
  a Windows runner.
- Normalize cwd comparison in resolveSessionLogByCwdScan: Windows is
  case-insensitive and processes may report drive letters in either
  case, so fold slashes and lowercase before comparing.

Windows viability confirmed empirically: ~/.claude/sessions/<pid>.json
exists with the expected schema on Windows 11 + Claude Code 2.1.116.

For provenance purposes, this commit was AI assisted.
@backnotprop
Copy link
Copy Markdown
Owner

I've added Windows support.

@backnotprop backnotprop merged commit 24a070d into backnotprop:main Apr 21, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/plannotator-last resolves wrong session when multiple Claude Code sessions share a repo

2 participants